/**************************************************************************************************
	$Header: /pub/cvsroot/mydns/src/mydns/axfr.c,v 1.2 2002/07/25 18:51:20 bboy Exp $
	axfr.c: Routines to do zone transfers.

	Copyright (C) 2002  Don Moore <bboy@bboy.net>

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at Your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
**************************************************************************************************/

#include "named.h"

#define	AXFR_TIME_LIMIT		3600		/* AXFR may not take more than this long, overall */

static size_t total_records, total_octets;


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_ERROR
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void axfr_error(TASK *, const char *, ...) __attribute__ ((__noreturn__));
static void
axfr_error(TASK *t, const char *fmt, ...)
{
	va_list ap;
	char msg[BUFSIZ];

	va_start(ap, fmt);
	vsnprintf(msg, sizeof(msg), fmt, ap);
	va_end(ap);

	if (t)
		Warnx("%s: %s", desctask(t), msg);
	else
		Warnx("%s", msg);

#if HAVE_SHUTDOWN
	shutdown(t->fd, 2);
#endif
	close(t->fd);
	_exit(EXIT_FAILURE);
}
/*--- axfr_error() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_TIMEOUT
	Hard timeout called by SIGALRM after one hour.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_timeout(int dummy)
{
	axfr_error(NULL, "AXFR timed out");
}
/*--- axfr_timeout() ----------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_WRITE_WAIT
	Wait for the client to become ready to read.  Times out after `task_timeout' seconds.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_write_wait(t)
	TASK *t;
{
	fd_set wfd;
	struct timeval tv;
	int rv;

	FD_ZERO(&wfd);
	FD_SET(t->fd, &wfd);
	tv.tv_sec = task_timeout;
	tv.tv_usec = 0;
	if ((rv = select(t->fd+1, NULL, &wfd, NULL, &tv)) < 0)
		axfr_error(t, "%s: %s", _("select"), strerror(errno));
	if (rv != 1 || !FD_ISSET(t->fd, &wfd))
		axfr_error(t, _("write timeout"));
}
/*--- axfr_write_wait() -------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_WRITE
	Writes the specified buffer, obeying task_timeout (via axfr_write_wait).
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_write(t, buf, size)
	TASK *t;
	u_char *buf;
	size_t size;
{
	int rv, offset = 0;

	do
	{
		axfr_write_wait(t);
		if ((rv = write(t->fd, buf+offset, size-offset)) < 0)
			axfr_error(t, "write: %s", strerror(errno));
		if (!rv)
			axfr_error(t, _("client closed connection"));
		offset += rv;
	} while (offset < size);
}
/*--- axfr_write() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_REPLY
	Sends one reply to the client.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_reply(t)
	TASK *t;
{
	int n;
	u_char len[2], *l = len;

	Assert(t);
	build_reply(t, 0);
	DNS_PUT16(l, t->replylen);
	axfr_write(t, len, SIZE16);
	axfr_write(t, t->reply, t->replylen);
	total_octets += SIZE16 + t->replylen;
	total_records++;

	/* Reset the pertinent parts of the task reply data */
	rrlist_free(&t->an);
	rrlist_free(&t->ns);
	rrlist_free(&t->ar);

	if (t->reply) free(t->reply);
	t->reply = NULL;
	t->replylen = 0;

	for (n = 0; n < t->numNames; n++)
		free(t->Names[n]);
	if (t->Names) free(t->Names);
	t->Names = NULL;
	t->numNames = 0;

	if (t->Offsets) free(t->Offsets);
	t->Offsets = NULL;

	if (t->rdata) free(t->rdata);
	t->rdata = NULL;
	t->rdlen = 0;

	reply_init(t);
}
/*--- axfr_reply() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_ZONE
	Transfer a non-PTR zone.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_zone(t)
	TASK *t;
{
	MYDNS_SOA *soa = NULL;
	MYSQL_RES *res = NULL;
	MYSQL_ROW row;
	char query[256];

	/* Load the SOA for the requested zone */
	if (mydns_soa_load(mysql, &soa, t->qname) < 0)
		ErrSQL(mysql, "%s: %s", desctask(t), _("error loading zone"));
	if (!soa)
	{
		dnserror(t, DNS_R_REFUSED);
		axfr_reply(t);
		axfr_error(t, _("unknown zone"));
	}

	/* Send opening SOA record */
	Verbose("%s: %s", desctask(t), _("starting zone transfer"));
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)soa, soa->origin);
	axfr_reply(t);

	/* Get all resource records for zone */
	snprintf(query, sizeof(query), "SELECT "MYDNS_RR_FIELDS" FROM "MYDNS_RR_TABLE" WHERE zone=%u",
				soa->id);
	if (mysql_real_query(mysql, query, strlen(query)) || !(res = mysql_use_result(mysql)))
		ErrSQL(mysql, "%s: %s", desctask(t), _("error loading resource records"));
	while ((row = mysql_fetch_row(res)))
	{
		MYDNS_RR *rr;
		int len;

		if (!(rr = mydns_parse_rr(row)))
			continue;
		len = strlen(rr->name);
		if (LASTCHAR(rr->name) != '.')
		{
			if (*rr->name)
				strcat(rr->name, ".");
			strncat(rr->name, soa->origin, sizeof(rr->name) - len - 1);
		}
		rrlist_add(t, ANSWER, DNS_RRTYPE_RR, (void *)rr, rr->name);
		axfr_reply(t);
		mydns_rr_free(rr);
	}
	mysql_free_result(res);

	/* Send closing SOA record */
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)soa, soa->origin);
	axfr_reply(t);
}
/*--- axfr_zone() -------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR_PTR_ZONE
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
axfr_ptr_zone(t, label)
	TASK *t;
	u_char *label;
{
	u_char *c, n, ip[4];
	MYDNS_SOA soa;
	MYSQL_RES *res = NULL;
	MYSQL_ROW row;
	struct tm *tm;
	char query[256], hostname[256], serial[40];

	/* Split in-addr.arpa format address into quads */
	ip[0] = ip[1] = ip[2] = ip[3] = 0;
	for (c = label, n = 0; *c && n < 4; c++)
		if (c == label || *c == '.')
		{
			if (*c == '.') c++;
			ip[3-n] = atoi(c);
			n++;
		}
	if (n != 3)
	{
		dnserror(t, DNS_R_REFUSED);
		axfr_reply(t);
		axfr_error(t, _("invalid zone format"));
	}

	/* Get all ptr records for zone */
	snprintf(query, sizeof(query),
		"SELECT "MYDNS_PTR_FIELDS" FROM "MYDNS_PTR_TABLE" WHERE "
		"ip >= INET_ATON('%d.%d.%d.0') AND ip <= INET_ATON('%d.%d.%d.255')",
		ip[1], ip[2], ip[3], ip[1], ip[2], ip[3]);
	if (mysql_real_query(mysql, query, strlen(query)) || !(res = mysql_store_result(mysql)))
		ErrSQL(mysql, "%s: %s", desctask(t), _("error loading ptr records"));

	/* If no results were found, refuse request */
	if (!mysql_num_rows(res))
	{
		mysql_free_result(res);
		dnserror(t, DNS_R_REFUSED);
		axfr_reply(t);
		axfr_error(t, _("invalid zone"));
	}

	/* Generate and send a SOA record */
	if (gethostname(hostname, sizeof(hostname)-1))
		axfr_error(t, "%s: %s", _("error getting hostname"), strerror(errno));
	soa.id = 0;
	strncpy(soa.origin, t->qname, sizeof(soa.origin)-1);
	snprintf(soa.ns, sizeof(soa.ns), "%s.", hostname);
	snprintf(soa.mbox, sizeof(soa.mbox), "hostmaster.%s.", hostname);
	tm = gmtime(&current_time);
	snprintf(serial, sizeof(serial), "%04d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
	soa.serial = atou(serial);
	soa.refresh = DNS_DEFAULT_REFRESH;
	soa.retry = DNS_DEFAULT_RETRY;
	soa.expire = DNS_DEFAULT_EXPIRE;
	soa.minimum = DNS_DEFAULT_MINIMUM;
	soa.ttl = DNS_DEFAULT_TTL;
	soa.next = NULL;

	Verbose("%s: %s", desctask(t), _("starting zone transfer"));
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)&soa, soa.origin);
	axfr_reply(t);

	/* Now send the PTR records */
	while ((row = mysql_fetch_row(res)))
	{
		MYDNS_PTR *ptr;
		u_char q[4], fqdn[DNS_MAXNAMELEN];

		if (!(ptr = mydns_parse_ptr(row)))
			continue;

		memcpy(&q, &ptr->ip, sizeof(q));
		snprintf(fqdn, sizeof(fqdn), "%d.%d.%d.%d.in-addr.arpa.", q[0], q[1], q[2], q[3]);
		rrlist_add(t, ANSWER, DNS_RRTYPE_PTR, (void *)ptr, fqdn);
		axfr_reply(t);
		mydns_ptr_free(ptr);
	}
	mysql_free_result(res);

	/* Send closing SOA record */
	rrlist_add(t, ANSWER, DNS_RRTYPE_SOA, (void *)&soa, soa.origin);
	axfr_reply(t);
}
/*--- axfr_ptr_zone() ---------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	AXFR
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void
axfr(t)
	TASK *t;
{
	struct timeval start, finish;
	int len;

	Assert(t);

	signal(SIGALRM, axfr_timeout);
	alarm(AXFR_TIME_LIMIT);
	db_connect();
	gettimeofday(&start, NULL);
	total_records = total_octets = 0;

#define PTR_SUFFIX_LEN	14	/* strlen(".in-addr.arpa.") */
	len = strlen(t->qname);
	if (len > PTR_SUFFIX_LEN && !strcmp(t->qname + (len-PTR_SUFFIX_LEN), ".in-addr.arpa."))
	{
		u_char label[DNS_MAXNAMELEN];

		memcpy(label, t->qname, len-PTR_SUFFIX_LEN);
		label[len-PTR_SUFFIX_LEN] = '\0';
		axfr_ptr_zone(t, label);
	}
	else
		axfr_zone(t);

	gettimeofday(&finish, NULL);
	Verbose(_("%s: zone transfer complete in %.3fs (%u records, %u octets)"), desctask(t),
		((finish.tv_sec + finish.tv_usec / 1000000.0) - (start.tv_sec + start.tv_usec / 1000000.0)),
		total_records, total_octets);

#if HAVE_SHUTDOWN
	shutdown(t->fd, 2);
#endif
	close(t->fd);
	_exit(EXIT_SUCCESS);
}
/*--- axfr() ------------------------------------------------------------------------------------*/

/* vi:set ts=3: */
